/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.swf.tools.abcdump
{
	import sweezy.swf.abc.opcode.Newfunction;
	import sweezy.swf.abc.structure.ClassInfo;
	import sweezy.swf.abc.structure.FunctionTrait;
	import sweezy.swf.abc.structure.GetterTrait;
	import sweezy.swf.abc.structure.InstanceInfo;
	import sweezy.swf.abc.structure.MethodBodyInfo;
	import sweezy.swf.abc.structure.MethodInfo;
	import sweezy.swf.abc.structure.MethodTrait;
	import sweezy.swf.abc.structure.NamespaceInfo;
	import sweezy.swf.abc.structure.OptionDetail;
	import sweezy.swf.abc.structure.QNameInfo;
	import sweezy.swf.abc.structure.ScriptInfo;
	import sweezy.swf.abc.structure.SetterTrait;
	import sweezy.swf.abc.structure.TraitInfo;
	import sweezy.swf.constant.MethodInfoFlags;
	import sweezy.swf.constant.NamespaceKind;
	import sweezy.swf.constant.TraitAttributes;
	import sweezy.swf.constant.TraitType;

	internal class FunctionDef extends ABCElementDef
	{

		private static const CINIT:uint = 0x02;

		private static const FUNCTION:uint = 0x08;

		private static const GETTER:uint = 0x10;

		private static const IINIT:uint = 0x04;

		private static const INIT:uint = 0x01;

		private static const METHOD:uint = 0x40;

		private static const NEWFUNCTION:uint = 0x80;

		private static const SETTER:uint = 0x20;

		private var _declaringClassDef:ClassDef;

		private var _flags:uint;

		private var _isStatic:Boolean;

		private var _methodInfo:MethodInfo;

		private var _object:Object;

		public function FunctionDef(container:ABCcontainerDef, declaringClassDef:ClassDef, object:Object, isStatic:Boolean)
		{
			super(container);

			_declaringClassDef = declaringClassDef;
			_object = object;
			_isStatic = isStatic;

			_flags = 0;
			if (_object is ScriptInfo)
			{
				_flags = INIT;
			}
			else if (_object is ClassInfo)
			{
				_flags = CINIT;
			}
			else if (_object is InstanceInfo)
			{
				_flags = IINIT;
			}
			else if (_object is Newfunction)
			{
				_flags = NEWFUNCTION;
			}
			else if (_object is TraitInfo)
			{
				switch ((_object as TraitInfo).traitType)
				{
					case TraitType.FUNCTION:
						_flags = FUNCTION;
						break;
					case TraitType.METHOD:
						_flags = METHOD;
						break;
					case TraitType.GETTER:
						_flags = GETTER;
						break;
					case TraitType.SETTER:
						_flags = SETTER;
						break;
				}
			}

			_methodInfo = _abc.methodInfos[methodIndex];
		}

		override public function dump(stream:IDumpStream):void
		{
			dumpMetadata(stream);

			var result:String = "";
			var methodBodyInfo:MethodBodyInfo = _container.findMethodBody(methodIndex);
			var codeDef:CodeDef = methodBodyInfo === null ? null : new CodeDef(_container, methodBodyInfo);
			var isConstructor:Boolean = _object is InstanceInfo;
			var namespace:Namespace = this.namespace;
			var name:String = this.name;

			if (isOverride)
			{
				result += "override ";
			}
			if ((_methodInfo.flags & MethodInfoFlags.NATIVE) === MethodInfoFlags.NATIVE)
			{
				result += "native ";
			}

			if (namespace !== null)
			{
				result += namespace;
				result += " ";
			}
			else if (isConstructor)
			{
				result += "public ";
			}

			if (isFinal)
			{
				result += "final ";
			}
			if (_isStatic)
			{
				result += "static ";
			}
			result += "function ";

			if ((_flags & GETTER) !== 0)
			{
				result += "get ";
			}
			else if ((_flags & SETTER) !== 0)
			{
				result += "set ";
			}

			if (name)
			{
				result += name;
				result += " ";
			}
			else if (isConstructor)
			{
				result += (_declaringClassDef as ClassDef).simpleName;
				result += " ";
			}

			result = result.substring(0, result.length - 1);
			result += "(";
			var delim:String = "";
			for each (var parameter:String in parameters)
			{
				result += delim;
				result += parameter;
				delim = ", ";
			}
			if ((_methodInfo.flags & MethodInfoFlags.NEED_REST) === MethodInfoFlags.NEED_REST)
			{
				result += delim;
				result += "...rest";
			}
			result += ")";

			if (!isConstructor)
			{
				result += ":";
				result += returnType;
			}

			if (codeDef === null)
			{
				result += ";";
			}

			var dispId:uint = 0;
			if (_object is GetterTrait)
			{
				dispId = (_object as GetterTrait).dispId;
			}
			else if (_object is SetterTrait)
			{
				dispId = (_object as SetterTrait).dispId;
			}
			else if (_object is MethodTrait)
			{
				dispId = (_object as MethodTrait).dispId;
			}

			result += " //disp_id=";
			result += dispId;
			result += ", method_id=";
			result += methodIndex;

			stream.println(result);
			if (codeDef === null)
			{
				return;
			}

			stream.println("{");
			stream.indentLevel++;
			codeDef.dump(stream);
			stream.indentLevel--;
			stream.println("}");
		}

		internal function get methodIndex():int
		{
			switch (_flags)
			{
				case INIT:
					return (_object as ScriptInfo).init;
				case CINIT:
					return (_object as ClassInfo).cinit;
				case IINIT:
					return (_object as InstanceInfo).iinit;
				case FUNCTION:
					return (_object as FunctionTrait).functionIndex;
				case NEWFUNCTION:
					return (_object as Newfunction).index;
				case METHOD:
					return (_object as MethodTrait).method;
				case GETTER:
					return (_object as GetterTrait).method;
				case SETTER:
					return (_object as SetterTrait).method;
			}

			return -1;
		}

		private function dumpMetadata(stream:IDumpStream):void
		{
			switch (_flags)
			{
				case INIT:
				case CINIT:
				case IINIT:
				case NEWFUNCTION:
					break;
				case FUNCTION:
				case METHOD:
				case GETTER:
				case SETTER:
					var trait:TraitInfo = _object as TraitInfo;
					if ((trait.attributes & TraitAttributes.METADATA) === TraitAttributes.METADATA)
					{
						for each (var index:uint in trait.metadata)
						{
							new MetadataDef(_container, _abc.metadataInfos[index]).dump(stream);
						}
					}
					break;
			}
		}

		private function getParameterString(_type:uint, _name:uint, _option:OptionDetail, _index:uint):String
		{
			var result:String = "";
			result += _name === 0 ? ("arg" + _index) : _constantPool.stringConstants[_name];
			result += ":";
			result += _type === 0 ? "*" : Utils.multinameToString(_constantPool, _type);
			if (_option !== null)
			{
				result += " = ";
				result += Utils.getConstantValueToString(_constantPool, _option.val, _option.kind);
			}

			return result;
		}

		private function getQname():QNameInfo
		{
			var name:uint = 0;

			switch (_flags)
			{
				case INIT:
				case CINIT:
				case IINIT:
				case NEWFUNCTION:
					break;
				case FUNCTION:
				case METHOD:
				case GETTER:
				case SETTER:
					name = (_object as TraitInfo).name;
					break;
			}

			if (name === 0)
			{
				return null;
			}

			return _constantPool.multinameInfos[name] as QNameInfo;
		}

		private function get isFinal():Boolean
		{
			switch (_flags)
			{
				case INIT:
				case CINIT:
				case IINIT:
				case FUNCTION:
				case NEWFUNCTION:
					return false;
				case METHOD:
				case GETTER:
				case SETTER:
					break;
			}

			return ((_object as TraitInfo).attributes & TraitAttributes.FINAL) === TraitAttributes.FINAL;
		}

		private function get isOverride():Boolean
		{
			switch (_flags)
			{
				case INIT:
				case CINIT:
				case IINIT:
				case FUNCTION:
				case NEWFUNCTION:
					return false;
				case METHOD:
				case GETTER:
				case SETTER:
					break;
			}

			return ((_object as TraitInfo).attributes & TraitAttributes.OVERRIDE) === TraitAttributes.OVERRIDE;
		}

		private function get name():String
		{
			var qname:QNameInfo = getQname();
			if (qname === null)
			{
				return "";
			}

			if (qname.name === 0)
			{
				return "";
			}

			return _constantPool.stringConstants[qname.name];
		}

		private function get namespace():Namespace
		{
			var qname:QNameInfo = getQname();
			if (qname === null)
			{
				return null;
			}

			var namespace:NamespaceInfo = _constantPool.namespaceInfos[qname.ns] as NamespaceInfo;
			if (namespace === null)
			{
				return null;
			}

			switch (namespace.kind)
			{
				case NamespaceKind.NAMESPACE:
					return new Namespace(_constantPool.stringConstants[namespace.name]);
				case NamespaceKind.PACKAGE_INTERNAL_NS:
					return Utils.INTERNAL;
				case NamespaceKind.PRIVATE_NS:
					return Utils.PRIVATE;
				case NamespaceKind.PACKAGE_NAMESPACE:
					return Utils.PUBLIC;
				case NamespaceKind.PROTECTED_NAMESPACE:
					return Utils.PROTECTED;
			}

			return null;
		}

		private function get parameters():Array
		{
			var parameterDefs:Array = [];
			var types:Array = _methodInfo.parameterTypes;
			var names:Array = _methodInfo.parameterNames;
			if (names === null)
			{
				names = [];
			}
			var options:Array = _methodInfo.options;
			if (options === null)
			{
				options = [];
			}
			var offset:uint = types.length - options.length;

			for (var i:uint = 0, len:uint = types.length; i < len; i++)
			{
				parameterDefs[i] = getParameterString(types[i], names[i], i < offset ? null : options[i - offset], i);
			}

			return parameterDefs;
		}

		private function get returnType():String
		{
			if (_methodInfo.returnType === 0)
			{
				return "*";
			}

			return Utils.multinameToString(_constantPool, _methodInfo.returnType);
		}
	}
}
